/*
 * Copyright (C) 2010-2011 Comcast Cable.  All Rights Reserved.
 */
package com.hercules.android.receiver.handlers.events.eventraiser;

import com.hercules.android.receiver.AndroidReceiver;
import com.hercules.android.receiver.ApplicationData;
import com.hercules.android.receiver.LogUtil;
import com.hercules.android.receiver.connection.ServerConnectionException;
import com.hercules.android.receiver.handlers.commands.XREAggregateCommandHandler;
import com.hercules.android.receiver.handlers.events.EventRaiser;
import com.hercules.android.receiver.handlers.events.EventRaisingException;
import com.hercules.android.receiver.handlers.events.EventsUtil;
import com.hercules.android.receiver.model.commands.XRECallCommand;
import com.hercules.android.receiver.model.commands.XRECommand;
import com.hercules.android.receiver.model.commands.XREConnectCommand;
import com.hercules.android.receiver.model.commands.XREConnectRejectedCommand;
import com.hercules.android.receiver.model.commands.XREDeleteCommand;
import com.hercules.android.receiver.model.commands.XRENewCommand;
import com.hercules.android.receiver.model.commands.XRERedirectCommand;
import com.hercules.android.receiver.model.commands.XRERestartCommand;
import com.hercules.android.receiver.model.commands.XRESetCommand;
import com.hercules.android.receiver.model.commands.XREShutdownCommand;
import com.hercules.android.receiver.model.commands.XRETargetCommand;
import com.hercules.android.receiver.model.events.XREEvent;
import com.hercules.android.receiver.model.events.XREEventPhase;
import com.hercules.android.receiver.model.events.XREKeyboardEvent;
import com.hercules.android.receiver.model.events.XREMouseEvent;
import com.hercules.android.receiver.model.events.XREResourceEvent;
import com.hercules.android.receiver.model.events.XREViewEvent;
import com.hercules.android.receiver.model.events.XREVirtualKeyCode;
import com.hercules.android.receiver.model.objects.XREApplication;
import com.hercules.android.receiver.model.objects.XRECommandSequence;
import com.hercules.android.receiver.model.objects.XREEventHandler;
import com.hercules.android.receiver.model.objects.XREObject;
import com.hercules.android.receiver.model.objects.XREResource;
import com.hercules.android.receiver.model.objects.XREView;
import com.hercules.android.receiver.util.evaluators.InvalidAndroidReceiverStateException;
import com.hercules.android.receiver.util.evaluators.InvalidPathException;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Logger;

/**
 * <p>
 * This is the default implementation of EventRaiser. It raises XREViewEvent and XREResourceEvent in different ways. For
 * XREViewEvent, it searches up the view hierarchy. For XREResourceEvent, it searches up the resource hierarchy in
 * consideration of nested applications. It finds a XREEventHandler whose filtering conditions match the given event,
 * then either raise it to server or handle it with {@link XREAggregateCommandHandler}.
 * <p>
 * <p>
 * <strong>Thread safety: </strong> This class is thread-safe because it's immutable.
 * <p>
 *
 * @author mekanizumu, jaindsonvs
 * @author 7realm
 * @version 1.0
 */
public class EventRaiserImpl implements EventRaiser {
    /**
     * <p>
     * The logger used for logging.
     * <p>
     * <p>
     * Initialization and Mutability: It is final and won't change once it is initialized as part of variable
     * declaration to: Logger.getLogger(EventRaiserImpl.class.getName()).
     * </p>
     * <p>
     * Usage: It's is used in all public methods.
     * </p>
     */
    private static final Logger LOGGER = LogUtil.getLogger(EventRaiserImpl.class.getSimpleName());

    /**
     * <p>
     * This is the default constructor for the class.
     * <p>
     */
    public EventRaiserImpl() {
        // empty
    }

    /**
     * <p>
     * Raise a XRE event either to server or to local receiver (depending on the value of toServerOnly).
     * </p>
     *
     * @param toServerOnly this is true if the event should always be raised to server.
     * @param receiver the receiver used for raising the event.
     * @param event the event to raise.
     * @return true if the event is raised, false otherwise.
     * @throws EventRaisingException if any error occurs
     * @throws IllegalArgumentException if event is null; or receiver is null;
     */
    @SuppressWarnings("boxing")
    public boolean raiseEvent(XREEvent event, AndroidReceiver receiver, boolean toServerOnly)
        throws EventRaisingException {
        final String methodName = "raiseEvent";
        LogUtil.enter(LOGGER, this, methodName);

        try {
            EventsUtil.checkArgumentNull(event, "event");
            EventsUtil.checkArgumentNull(receiver, "receiver");
        } catch (IllegalArgumentException e) {
            throw LogUtil.error(LOGGER, this, methodName, e);
        }

        try {
            LogUtil.info(LOGGER, this, methodName, "Raising event {0}, source {1}", event.getName(), event.getSource());

            if (receiver.getApplicationData().getElementsXREIdToObject().get(event.getSource()) == null) {
            	LogUtil.error(LOGGER, this, methodName, new EventRaisingException("receiver.getApplicationData().getElementsXREIdToObject().get["
                        + event.getSource() + "] is null"));
            	return false;
            }
            boolean result = doRaiseEvent(event, receiver, toServerOnly);
            return LogUtil.exit(LOGGER, this, methodName, result);
        } catch (ClassCastException e) {
            throw LogUtil.error(LOGGER, this, methodName, new EventRaisingException("Wrong class found.", e));
        } catch (IllegalArgumentException e) {
            // throw LogUtil.error(LOGGER, this, methodName, new EventRaisingException("Wrong argument passed.", e));
        	LogUtil.error(LOGGER, this, methodName, new EventRaisingException("Wrong argument passed. AppName: " + receiver.getApplicationConnectData().getApplicationName(), e));
        	return false;
        } finally {
            LogUtil.exit(LOGGER, this, methodName);
        }
    }

    /**
     * <p>
     * Raise a XRE event either to server or to local receiver (depending on the value of toServerOnly).
     * </p>
     *
     * @param toServerOnly this is true if the event should always be raised to server.
     * @param receiver the receiver used for raising the event.
     * @param event the event to raise.
     * @return true if the event is raised, false otherwise.
     * @throws EventRaisingException if any error occurs
     */
    @SuppressWarnings("boxing")
    private boolean doRaiseEvent(XREEvent event, AndroidReceiver receiver, boolean toServerOnly)
        throws EventRaisingException {
        ApplicationData appData = receiver.getApplicationData();
        Map<Integer, XREObject> elementsXREIdToObject = appData.getElementsXREIdToObject();
        EventsUtil.checkObjectNull(elementsXREIdToObject, "receiver.getApplicationData().getElementsXREIdToObject()");
        XREObject source = elementsXREIdToObject.get(event.getSource());
        EventsUtil.checkObjectNull(source, "receiver.getApplicationData().getElementsXREIdToObject().get["
            + event.getSource() + "]");

        if (event instanceof XREViewEvent) {
            // collects all parent views hierarchy, reverse it for PREVIEW event
        	if (event.getPhase() == XREEventPhase.PREVIEW) {
        		AndroidReceiver curReceiver = receiver;
        		List<AndroidReceiver> receivers = new ArrayList<AndroidReceiver>();
        		while (curReceiver != null) {
        			receivers.add(curReceiver);
        			curReceiver = curReceiver.getApplicationData().getParentReceiver();
        		}
        		Collections.reverse(receivers);
        		
        		for (int i = 0; i < receivers.size(); ++i) {
        			List<XREView> allParentViews = null;
        			curReceiver = receivers.get(i);
        			
        			if (i == receivers.size() - 1) {
        				allParentViews = findAllParentViews((XREView) source);
        			} else {
        				AndroidReceiver childReceiver = receivers.get(i + 1);
        				XREView startView = childReceiver.getApplicationData().getParentXREApplicationView();
        				allParentViews = findAllParentViews(startView);
        			}
        			
        			Collections.reverse(allParentViews);
        			
        			for (XREView currentView : allParentViews) {
	                    // AndroidAssembly3 stop handling event if it was handled
	                    handleEvent(currentView, event, curReceiver, toServerOnly);
	                }
        		}
        	} else {
        		AndroidReceiver curReceiver = receiver;
        		do {
	        		List<XREView> allParentViews = findAllParentViews((XREView) source);
	        		// bubble command
	                for (XREView currentView : allParentViews) {
	                    // AndroidAssembly3 stop handling event if it was handled
	                    if (handleEvent(currentView, event, curReceiver, toServerOnly)) {
	                        return true;
	                    }
	                }
	                
	                source = curReceiver.getApplicationData().getParentXREApplicationView();
	                curReceiver = curReceiver.getApplicationData().getParentReceiver();
        		} while (curReceiver != null);
        	}
        } else if (event instanceof XREResourceEvent) {
        	event.setPhase(XREEventPhase.STANDARD);
            // check if current resource can handle the event
            if (handleEvent(source, event, receiver, true)) {
                return true;
            }

            // try to handle command within application hierarchy
            AndroidReceiver currentReceiver = receiver;
            while (currentReceiver != null) {
                // try to handle with current application
                XREResource currentApplication =
                    (XREApplication) elementsXREIdToObject.get(XREObject.ID_ROOT_APPLICATION);
                EventsUtil.checkObjectNull(currentApplication,
                    "(XREApplication) elementsXREIdToObject.get(XREObject.ID_ROOT_APPLICATION)");
                if (handleEvent(currentApplication, event, receiver, true)) {
                    return true;
                }

                // try to handle with resource that contains application
                XREView parentXREApplicationView = currentReceiver.getApplicationData().getParentXREApplicationView();
                if (parentXREApplicationView != null) {
                    // AndroidAssembly2 checked if parentXREApplicationView is not null because this causes NPE
                    currentApplication = parentXREApplicationView.getResource();
                    if (handleEvent(currentApplication, event, receiver, true)) {
                        return true;
                    }
                }

                // move to parent
                currentReceiver = currentReceiver.getApplicationData().getParentReceiver();
            }
        } else {
            // AndroidAssembly3 added support for plain events
            LogUtil.warn(LOGGER, this, "raiseEvent", "Unknown event is raised {0}.", event.getClass().getName());
        }

        return false;
    }

    /**
     * <p>
     * Find all parent views starting from sourceView, including sourceView.
     * </p>
     *
     * @param sourceView the source view to start from.
     * @return all parent views starting from sourceView, including sourceView.
     */
    private static List<XREView> findAllParentViews(XREView sourceView) {
        List<XREView> result = new ArrayList<XREView>();
        XREView currentView = sourceView;
        while (currentView != null) {
            result.add(currentView);
            currentView = currentView.getParent();
        }

        return result;
    }

    /**
     * <p>
     * Check if a XREEventHandler's filtering conditions match the properties of an XREEvent.
     * </p>
     * <p>
     * AndroidAssembly1 correct bug with no filters and field names
     *
     * @param event the event to match.
     * @param handler the handler to check.
     * @return true if the handler matches the event, false otherwise.
     * @throws EventRaisingException if any error occurs.
     */
    private boolean isMatched(XREEventHandler handler, XREEvent event) throws EventRaisingException {
        // Check all filters and return true if any one of them is satisfied.
        for (Map<String, Object> filterMap : handler.getFilter()) {
            boolean allConditionsMatch = true;
            // A filter is satisfied if all its conditions are satisfied as well. Check all of them.
            for (String key : filterMap.keySet()) {
                if ("phase".equals(key)) {
                    // Check if the event is in the expected phase
                    if (!filterMap.get(key).equals(event.getPhase().toString())) {
                        allConditionsMatch = false;
                        break;
                    }
                } else if (event instanceof XREMouseEvent && "button".equals(key)) {
                    // Check if the event is for the expected button
                    if (!filterMap.get(key).equals(((XREMouseEvent) event).getButton().toString())) {
                        allConditionsMatch = false;
                        break;
                    }
                } else if (event instanceof XREKeyboardEvent && "virtualKeyCode".equals(key)) {
                    // Check if the event is for the expected virtual key code
                    String keyName = (String) filterMap.get(key);
                    XREVirtualKeyCode virtualKeyCode = getVirtualKeyCode(keyName);
                    if (!virtualKeyCode.equals(((XREKeyboardEvent) event).getVirtualKeyCode())) {
                        allConditionsMatch = false;
                        break;
                    }
                } else {
                    // Use reflection to compare the field value
                    boolean equals = compareFieldValueUsingReflection(event, filterMap.get(key), key);
                    if (!equals) {
                        allConditionsMatch = false;
                        break;
                    }
                }
            }
            if (allConditionsMatch) {
                return true;
            }
        }

        return handler.getFilter().isEmpty();
    }

    /**
     * <p>
     * Returns a XREVirtualKeyCode instance stored in a static field in {@link XREVirtualKeyCode} class.
     * </p>
     *
     * @param fieldName the name of the field which holds the XREVirtualKeyCode instance to be retrieved.
     * @return a XREVirtualKeyCode instance.
     * @throws EventRaisingException if some error occurs while retrieving the XREVirtualKeyCode.
     */
    private XREVirtualKeyCode getVirtualKeyCode(String fieldName) throws EventRaisingException {
        final String methodName = "getVirtualKeyCode";
        Field field = null;
        XREVirtualKeyCode virtualKeyCode = null;

        // Get the declared field that holds the instance.
        try {
            field = XREVirtualKeyCode.class.getDeclaredField(fieldName);
        } catch (SecurityException e) {
            throw LogUtil.error(LOGGER, this, methodName, new EventRaisingException(String.format(
                "Error! Security error occurred while getting " + "declared field \"%s\" for XREVirtualKeyCode class.",
                fieldName), e));
        } catch (NoSuchFieldException e) {
            throw LogUtil.error(LOGGER, this, methodName, new EventRaisingException(String.format(
                "Error! Could not find declared field \"%s\" on XREVirtualKeyCode class.", fieldName), e));
        }

        // Retrieve the instance from the field.
        try {
            virtualKeyCode = (XREVirtualKeyCode) field.get(null);
        } catch (IllegalArgumentException e) {
            throw LogUtil.error(LOGGER, this, methodName, new EventRaisingException(
                "Object not compatible with the declaring class: XREVirtualKeyCode class.", e));
        } catch (IllegalAccessException e) {
            throw LogUtil.error(LOGGER, this, methodName, new EventRaisingException(String.format(
                "Field \"%s\" is not accessible on XREVirtualKeyCode class.", fieldName), e));
        }
        return virtualKeyCode;
    }

    /**
     * <p>
     * Compares the field value using reflection and returns false if the objects do not match. We use the getter for
     * the field to retrieve its value.
     * </p>
     *
     * @param event the event that has the field that holds the object to be compared.
     * @param obj the expected object.
     * @param fieldName the name of the field that holds the object to be compared.
     * @return true if the objects match, false otherwise.
     * @throws EventRaisingException if any error occurs.
     */
    private boolean compareFieldValueUsingReflection(XREEvent event, Object obj, String fieldName)
        throws EventRaisingException {
        final String methodName = "compareFieldValueUsingReflection";
        Object instance = null;

        Method method = null;

        String getMethodName = getGetterName(fieldName);

        try {
            method = event.getClass().getDeclaredMethod(getMethodName);
        } catch (SecurityException e) {
            throw LogUtil.error(LOGGER, this, methodName, new EventRaisingException(String.format(
                "Error! Security error occurred while getting declared method \"%s\".", getMethodName), e));
        } catch (NoSuchMethodException e) {
            throw LogUtil.error(LOGGER, this, methodName, new EventRaisingException(String.format(
                "Error! Could not find declared method \"%s\".", getMethodName), e));
        }

        try {
            instance = method.invoke(event);
        } catch (IllegalArgumentException e) {
            throw LogUtil.error(LOGGER, this, methodName, new EventRaisingException(String.format(
                "Error! Object does not have get method for field \"%s\" with no args.", getMethodName), e));
        } catch (IllegalAccessException e) {
            throw LogUtil.error(LOGGER, this, methodName, new EventRaisingException(String.format(
                "Error! Method \"%s\" is not accessible.", fieldName), e));
        } catch (InvocationTargetException e) {
            throw LogUtil.error(LOGGER, this, methodName, new EventRaisingException(String.format(
                "Error! Exception was thrown by the get method: \"%s\".", fieldName), e));
        }
        return obj.equals(instance);
    }

    /**
     * <p>
     * Returns the proper name of the GET method of a given field.
     * </p>
     *
     * @param fieldName the name of the field.
     * @return the proper name of the GET method of the field.
     */
    private static String getGetterName(String fieldName) {
        return "get" + fieldName.substring(0, 1).toUpperCase() + fieldName.substring(1);
    }

    /**
     * <p>
     * Try to handle a XRE event with the XREEventHandler from xreObject. The event will be handled only if the handler
     * from xreObject matches the event's properties.
     * </p>
     *
     * @param toServerOnly this is true if the event should always be raised to server.
     * @param receiver the receiver used for handling the event.
     * @param event the event to handle.
     * @param xreObject the XRE object with XREEventHandler.
     * @return true if the event is handled, false otherwise.
     * @throws EventRaisingException if any error occurs.
     */
    @SuppressWarnings("boxing")
    private boolean handleEvent(XREObject xreObject, XREEvent event, AndroidReceiver receiver, boolean toServerOnly)
        throws EventRaisingException {
        String methodName = "handleEvent";
        Map<String, XREEventHandler> eventHandlers = xreObject.getEventHandlers();
        if (eventHandlers == null) {
            return false;
        }

        // get handler for event
        XREEventHandler handler = eventHandlers.get(event.getName());
        if (handler == null) {
            return false;
        }

        // check if event is matched by handler
        LogUtil.info(LOGGER, this, methodName, "Handle event {0} for {1}, check if it is matched.", event.getName(),
            xreObject.getId());
        if (isMatched(handler, event)) {
            LogUtil.info(LOGGER, this, methodName, "Event {0} matched for {1}. Sending it to server.", event.getName(),
                xreObject.getId());
            event.setHandler(xreObject.getId());

            try {
                // send event to server
                receiver.getServerConnection().send(event, receiver);
            } catch (ServerConnectionException e) {
                throw LogUtil.error(LOGGER, this, "tryHandleEventToServerOnly", new EventRaisingException(
                    "Error occurred while sending data to server.", e));
            }

            // AndroidAssembly2 send also event to installed handlers
            if (!toServerOnly) {
                // it means we handle the event locally
                handleEventLocally(receiver, xreObject, handler.getCommandSequence());
                LogUtil.info(LOGGER, this, methodName, "Event {0} for {1} is also handled locally, command "
                    + "sequence {2}.", event.getName(), xreObject.getId(), handler.getCommandSequence());
            }
            return true;
        }

        return false;
    }

    /**
     * <p>
     * Tries to handle the event not to server only.
     * </p>
     * <p>
     * AndroidAssembly2 added xreObject parameter
     *
     * @param receiver the receiver used for handling the event.
     * @param xreObject the source of event
     * @param commandSequence the sequence of commands to be handled.
     * @throws EventRaisingException if any error occurs.
     */
    private void handleEventLocally(AndroidReceiver receiver, XREObject xreObject, XRECommandSequence commandSequence)
        throws EventRaisingException {
        final String methodName = "tryHandleEventNotToServerOnly";

        if (commandSequence == null) {
            return;
        }
        XREAggregateCommandHandler commandHandler = receiver.getCommandHandler();
        try {
            for (XRECommand command : commandSequence.getCommands()) {
                command = copyCommand(command);
                // AndroidAssembly2 changed evaluation
                if (command instanceof XRETargetCommand) {
                    XRETargetCommand targetCommand = (XRETargetCommand) command;

                    String path = targetCommand.getTargetPath();
                    if (path != null && path.trim().length() != 0) {
                        targetCommand.setTargetId(commandHandler.getPathEvaluator().evaluatePath(path,
                            xreObject.getId(), receiver));
                    }
                }
                commandHandler.onCommandMessage(command, receiver);
            }
        } catch (IllegalArgumentException e) {
            throw LogUtil.error(LOGGER, this, methodName, new EventRaisingException(
                "Some path is null/empty or sourceId is not positive.", e));
        } catch (InvalidAndroidReceiverStateException e) {
            throw LogUtil.error(LOGGER, this, methodName, new EventRaisingException(
                "Receiver or some XRE object not properly initialized.", e));
        } catch (InvalidPathException e) {
            throw LogUtil.error(LOGGER, this, methodName, new EventRaisingException("Invalid XRE object path.", e));
        }
    }

    /**
     * <p>
     * Creates a copy of the command.
     * </p>
     * <p>
     * AndroidAssembly2 removed evaluation
     *
     * @param command the command to be copied.
     * @return an evaluated copy of the command.
     * @throws EventRaisingException if the concrete command is not a known implementation.
     */
    private static XRECommand copyCommand(XRECommand command) throws EventRaisingException {
        if (XRECallCommand.class.isAssignableFrom(command.getClass())) {
            return copy((XRECallCommand) command);
        } else if (XREConnectCommand.class.isAssignableFrom(command.getClass())) {
            return copy((XREConnectCommand) command);
        } else if (XREConnectRejectedCommand.class.isAssignableFrom(command.getClass())) {
            return copy((XREConnectRejectedCommand) command);
        } else if (XREDeleteCommand.class.isAssignableFrom(command.getClass())) {
            return copy((XREDeleteCommand) command);
        } else if (XRENewCommand.class.isAssignableFrom(command.getClass())) {
            return copy((XRENewCommand) command);
        } else if (XRERedirectCommand.class.isAssignableFrom(command.getClass())) {
            return copy((XRERedirectCommand) command);
        } else if (XRERestartCommand.class.isAssignableFrom(command.getClass())) {
            return copy((XRERestartCommand) command);
        } else if (XRESetCommand.class.isAssignableFrom(command.getClass())) {
            return copy((XRESetCommand) command);
        } else if (XREShutdownCommand.class.isAssignableFrom(command.getClass())) {
            return copy((XREShutdownCommand) command);
        }

        throw new EventRaisingException("Unexpected command type: " + command.getClass().getName());
    }

    /**
     * <p>
     * Creates a copy of the command and evaluates its target path if needed.
     * </p>
     * <p>
     * AndroidAssembly2 removed evaluation
     *
     * @param command the command to be copied and evaluated.
     * @return an evaluated copy of the command.
     */
    private static XRECallCommand copy(XRECallCommand command) {
        XRECallCommand copy = new XRECallCommand();
        copy.setMethod(command.getMethod());
        copy.setParams(new ArrayList<Object>(command.getParams()));
        copy.setTargetId(command.getTargetId());
        copy.setTargetPath(command.getTargetPath());
        copy.setTimestamp(command.getTimestamp());
        copy.setPathEvaluator(command.getPathEvaluator());
        return copy;
    }

    /**
     * <p>
     * Creates a copy of the command and evaluates its target path if needed.
     * </p>
     *
     * @param command the command to be copied and evaluated.
     * @return an evaluated copy of the command.
     */
    private static XREConnectCommand copy(XREConnectCommand command) {
        XREConnectCommand copy = new XREConnectCommand();
        copy.setCommandIndex(command.getCommandIndex());
        copy.setKeyMapURL(command.getKeyMapURL());
        copy.setSessionGUID(command.getSessionGUID());
        copy.setTimestamp(command.getTimestamp());
        copy.setVersion(command.getVersion());
        return copy;
    }

    /**
     * <p>
     * Creates a copy of the command and evaluates its target path if needed.
     * </p>
     *
     * @param command the command to be copied and evaluated.
     * @return an evaluated copy of the command.
     */
    private static XREConnectRejectedCommand copy(XREConnectRejectedCommand command) {
        XREConnectRejectedCommand copy = new XREConnectRejectedCommand();
        copy.setCommandIndex(command.getCommandIndex());
        copy.setReason(command.getReason());
        copy.setStatusCode(command.getStatusCode());
        copy.setTimestamp(command.getTimestamp());
        return copy;
    }

    /**
     * <p>
     * Creates a copy of the command and evaluates its target path if needed.
     * </p>
     * <p>
     * AndroidAssembly2 removed evaluation
     *
     * @param command the command to be copied and evaluated.
     * @return an evaluated copy of the command.
     */
    private static XREDeleteCommand copy(XREDeleteCommand command) {
        XREDeleteCommand copy = new XREDeleteCommand();
        copy.setTargetId(command.getTargetId());
        copy.setTargetPath(command.getTargetPath());
        copy.setTimestamp(command.getTimestamp());
        copy.setPathEvaluator(command.getPathEvaluator());
        return copy;
    }

    /**
     * <p>
     * Creates a copy of the command and evaluates its target path if needed.
     * </p>
     *
     * @param command the command to be copied and evaluated.
     * @return an evaluated copy of the command.
     */
    private static XRENewCommand copy(XRENewCommand command) {
        XRENewCommand copy = new XRENewCommand();
        copy.setCommandIndex(command.getCommandIndex());
        copy.setId(command.getId());
        copy.setKlass(command.getKlass());
        copy.setParams(new HashMap<String, Object>(command.getParams()));
        copy.setTimestamp(command.getTimestamp());
        return copy;
    }

    /**
     * <p>
     * Creates a copy of the command and evaluates its target path if needed.
     * </p>
     *
     * @param command the command to be copied and evaluated.
     * @return an evaluated copy of the command.
     */
    private static XRERedirectCommand copy(XRERedirectCommand command) {
        XRERedirectCommand copy = new XRERedirectCommand();
        copy.setCommandIndex(command.getCommandIndex());
        copy.setReason(command.getReason());
        copy.setStatusCode(command.getStatusCode());
        copy.setTimestamp(command.getTimestamp());
        copy.setUrl(command.getUrl());
        copy.setPreserveSession(command.isPreserveSession());
        return copy;
    }

    /**
     * <p>
     * Creates a copy of the command and evaluates its target path if needed.
     * </p>
     *
     * @param command the command to be copied and evaluated.
     * @return an evaluated copy of the command.
     */
    private static XRERestartCommand copy(XRERestartCommand command) {
        XRERestartCommand copy = new XRERestartCommand();
        copy.setCommandIndex(command.getCommandIndex());
        copy.setReason(command.getReason());
        copy.setStatusCode(command.getStatusCode());
        copy.setTimestamp(command.getTimestamp());
        return copy;
    }

    /**
     * <p>
     * Creates a copy of the command and evaluates its target path if needed.
     * </p>
     * <p>
     * AndroidAssembly2 removed evaluation
     *
     * @param command the command to be copied and evaluated.
     * @return an evaluated copy of the command.
     */
    private static XRESetCommand copy(XRESetCommand command) {
        XRESetCommand copy = new XRESetCommand();
        copy.setCommandIndex(command.getCommandIndex());
        copy.setTimestamp(command.getTimestamp());
        copy.setProps(new HashMap<String, Object>(command.getProps()));
        copy.setTargetId(command.getTargetId());
        copy.setTargetPath(command.getTargetPath());
        return copy;
    }

    /**
     * <p>
     * Creates a copy of the command and evaluates its target path if needed.
     * </p>
     *
     * @param command the command to be copied and evaluated.
     * @return an evaluated copy of the command.
     */
    private static XREShutdownCommand copy(XREShutdownCommand command) {
        XREShutdownCommand copy = new XREShutdownCommand();
        copy.setCommandIndex(command.getCommandIndex());
        copy.setTimestamp(command.getTimestamp());
        return copy;
    }

}
